Skip to content

feat(lint): rls-predicate-unknown-field / rls-predicate-unknown-user-variable — the reference half of the RLS predicate gate - #17036

Merged
baozhoutao merged 6 commits into
mainfrom
claude/issue-16119-rls-predicate-references
Sep 9, 2026
Merged

feat(lint): rls-predicate-unknown-field / rls-predicate-unknown-user-variable — the reference half of the RLS predicate gate#17036
baozhoutao merged 6 commits into
mainfrom
claude/issue-16119-rls-predicate-references

Conversation

@claude

@claude claude Bot commented Sep 9, 2026

Copy link
Copy Markdown
Contributor

Part of #16119

The three shipped rls-predicate-* rules judge a predicate's shape. Nothing judged what it points at. This adds the reference half beside them, without touching them.

The defect, re-measured on this tree before building

The card's four injections, driven through the whole AUTHORING_RULES table (not just this rule) at one site — a sales_manager set with opportunity_private_owner_only on a crm_opportunity that declares its columns:

using at that site before after
billing_address.country == "US" rls-predicate-unenforceable — CONTROL unchanged
is_private == = false rls-predicate-unparseable — CONTROL unchanged
is_private_nope == false || owner_id == current_user.id nothing rls-predicate-unknown-field
is_private == false || owner_id == current_user.nope nothing rls-predicate-unknown-user-variable
is_private == false || owner_id == current_user.id (shipped) nothing nothing — negative control

Both controls fired in the before leg, so the two zeros were never "the linter did not reach the site". The one non-RLS finding the table also produces (security-owd-unset, the fixture declares no sharingModel) is byte-identical across all five rows, so it is background rather than a second report — asserted mechanically in the suite, not by eye.

The card stands. Both silent rows were still silent on origin/main.

⚠️ The card's fail-CLOSED reading does not generalise — one direction is fail-OPEN

Raised in review and confirmed here, first-hand, before the prose was rewritten. The detection is unchanged; what the PR claimed the miss costs was wrong.

extractTargetField recognises only a leading field == / = / in. For a negation, or for any arm after the first, it returns null, the policy is kept, dropped never increments and the deny sentinel never arms. The phantom column then lowers to a negated constraint, and a row that has no such column satisfies it (noValueSatisfiesNegation: $ne / $nin / $notContains).

Measured on the driver-memory matcher, three rows, none carrying the column:

using net sees leading field policy rows matched
is_private == false — real narrowing, CONTROL is_private (declared) kept 1/3
nope == false — phantom, positive, CONTROL nope (missing) dropped → deny 0/3
nope != "x" null (not recognised) kept 3/3
!(nope == 1) null (not recognised) kept 3/3
!(nope in ['a']) null (not recognised) kept 3/3
is_private == false || nope != "x" is_private (declared) kept 3/3

The write path has the same asymmetry, measured against the same controls on matchesFilterCondition: a positive phantom constraint refuses the post-image, a negated one is satisfied vacuously and permits the write the policy was written to refuse.

Not a cross-tenant leak — tenancy is a separate layer and it holds; what is defeated is the narrowing authored inside the wall. driver-mongodb follows the same shared ruling; driver-sql is NOT MEASURED and is expected to fail closed by raising no such column.

The runtime repair is #17042 and is deliberately not attempted here. What changed in this PR is prose in three places — the finding's message, the file docblock and the changeset — so an author is never told "this denies everything" about a predicate that in fact matches everything. The unknown-variable half really is fail-closed in every position (the compiler refuses it under ! and in a trailing || arm alike), and its message still says so without qualification; a test pins that it does not acquire the fail-OPEN wording. Five more cases pin that the fail-open shapes are reported at all — a rule that only caught the leading position would have satisfied the card and missed the dangerous half.

What the estimate turned out to be

The dispatch was right and triage's "needs new capability" was stale. object-graph.ts was already imported by this file for recordsOf alone; reaching the field set was three names added to that existing import (indexObjectGraph, resolveFieldPath, isUnjudgeable, plus describeFieldPathVerdict / listNames / suggestName for the prose). No new input path was built.

How the two answers are obtained

Neither half re-parses the predicate — that fork is what this file's own docblock refuses.

Fields are read off the pushdown compiler's own output. compileCelToFilter lowers the predicate against a probe context; every producer of a field key in cel-to-filter.ts (emit, lowerMembership, lowerStringMethod) writes the path as the condition's key, so the lowered FilterCondition's keys are the columns the driver will be handed. Those resolve through object-graph.ts, inheriting its three skips unchanged.

Variables are discovered from the compiler's own refusal: an unbound key makes resolveValue raise unresolved-variable naming the path. The known set is derived, not transcribedRESERVED_RLS_MEMBERSHIP_KEYS from @objectstack/spec/contracts, the keys an IRlsMembershipResolver may never supply because the kernel already owns them, and the same set RLSCompiler.compileFilter builds its RLSUserContext from. A key added there stops being reported the same day, with no edit in this package. The card's five-name hotcrm guard was not copied, and RLSUserContextSchema in packages/spec/src/security/rls.zod.ts was deliberately not used — it still spells the org key tenantId and carries department / attributes the RLS compiler never binds, so reading it would judge policies against a shape the runtime does not have.

The boundary that lets the variable rule exist at all

An app stages arbitrary sets into ExecutionContext.rlsMembership and references them as membership tests; RowLevelSecurityPolicySchema documents the pattern under §7.3.1 and rls-predicate-unparseable's own hint recommends it. In an in position an unknown key is indistinguishable from a correct one and is never reported.

It is decidable in the other positions only because the merge is array-only — compileFilter stages an entry if (Array.isArray(value)) and never lets one clobber a named field, so the sole value an app-staged key can ever hold is an array, which a scalar position cannot use on any request. The position question is asked of the compiler, not of a model of it: bind the key to a scalar and re-run. So owner_id == current_user.nope is refused while assigned_to_id in current_user.team_member_ids stays silent, and a key used in both positions takes the membership answer — the conservative direction.

Corpus — one reading, and two zeros that are ⛔ NOT readings

The earlier revision of this section reported three zeros as if they were three readings. Two of them are vacuous, and the showcase figures were double-counted (the set is exported both under its own name and inside allPermissionSets, so every policy was counted twice). Corrected:

corpus RLS clauses judgeable new findings is it a reading?
examples/app-showcase 3 3 0 yes — controls below
plugin-security seed sets 71 0 0 no — every policy target is an object that package does not declare, so all 71 are skip-1 by construction
hotcrm built-permissions fixture 4 0 0 no — the fixture carries no objects key at all. Declare crm_opportunity and it reports 2, which is what a control is for

The showcase reading, at the real shipped site (showcase_contributor / task_own_rows on showcase_task, shipped using: assignee == current_user.email) — three firing controls and two nonsense controls:

  • injected dangling column → 1 · injected unknown scalar variable → 1 · injected fail-open negation shape → 1
  • injected membership test against an unknown key → 0 · real field + real variable → 0

Object count is 22 distinct objects as enumerated from examples/app-showcase/src/data/objects/*.ts; the load-bearing figure is the judgeable one, and all 3 clauses resolve against declared objects.

Reverse verification

The reference pass was ablated on the committed implementation — the call site replaced, the mutation proven on disk by blob hash (e1b5efaf… to 5dbe239f…) before any test ran. 22 tests failed, 64 still passed: the positives die, the skips and negative controls do not, which is the shape that distinguishes a real gate from an always-fires one. Restored by git checkout HEAD --, verified by blob-hash equality and an empty git diff HEAD, not by an exit code. (Re-run on the corrected tree; the first revision measured 17/63 before the five fail-open cases were added.)

Clause-② — re-derived, not copied

yes, agreeing with the dispatch. The diff adds two published exports to @objectstack/lint's public entry, and two members to the closed set of rule ids that allowlists and --json consumers key on (this file's own docblock states that consumers key on the id). Both are C5 widening tells. Publish set measured rather than assumed: files[] is ["dist","README.md","CHANGELOG.md"], and after a build both new ids appear in dist/index.js, dist/index.cjs, dist/runtime.js, dist/runtime.cjs, dist/index.d.ts and dist/index.d.cts — with a positive control (rls-predicate-unenforceable hits the same paths) and a negative control (an invented symbol hits nothing). So the changeset grades @objectstack/lint minor, never patch: exports and behaviour are added, nothing is removed or narrowed, and the three existing ids are byte-identical on every input.

Verification

All re-run on the final head 530469ff4c.

  • pnpm --filter @objectstack/lint test103 files / 3654 tests pass, 0 skipped. ⚠️ An intermediate run reported "5 skipped": lazy-deps.test.ts and runtime-lazy-deps.test.ts self-skip when packages/lint/dist is absent, and this package must be built before they measure anything. They police the dep-tier contract, which is exactly what a new @objectstack/spec/contracts import could disturb, so they are built and run rather than left silently skipped.
  • pnpm --filter @objectstack/lint typecheck — pass, including check:test-typecheck.
  • Gate families derived mechanically with scripts/pm/dispatch-gates.mjs --commands, then reconciled: 56 derived, 56 run, 0 UNRUN. 53 exit 0. Three exit 3 = PREREQUISITE NOT MET and print "this is NOT a pass and NOT a finding" themselves — check:lean-entry-closure, check:dual-build-cjs-loads, check:type-check-debt. All three need the full workspace build closure, which is CI's run; recorded as NOT MEASURED, not as green.
  • check:doc-authoring went red on the first draft of the corrected prose, and it was right: the new message cited #17042, and a runtime string reaches authors, operators and generated surfaces who cannot resolve a bare tracker id. The id moved to the adjacent // comment (the remedy the gate itself names) and a test now pins that neither the message nor the hint carries one. ⛔ No baseline entry was added — that path is maintainer-only.
  • Repo-wide lint was run in full, not narrowed: eslint . --no-inline-config over its own population of 6396 files — 0 errors, 0 warnings.

Notes for the reviewer


Generated by Claude Code

WIP: the reference half of the RLS predicate gate.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_012GKcPZbMoGq7WPzKLfRBTU
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_012GKcPZbMoGq7WPzKLfRBTU
@github-actions github-actions Bot added size/l documentation Improvements or additions to documentation tests tooling labels Sep 9, 2026
@github-actions

github-actions Bot commented Sep 9, 2026

Copy link
Copy Markdown
Contributor

📓 Docs Drift Check

This PR changes 1 package(s): @objectstack/lint, touching 20 documentable anchor(s). ⚠️ 1 changed file(s) yielded no anchor (packages/lint/src/index.ts), so the pages documenting them are NOT COVERED by this run — this is not a clean bill of health for those files.

23 hand-written doc(s) name something this change touched — list omitted above 15 rows. Re-derive on the tree named below: node scripts/docs-audit/affected-docs.mjs --json c0e4bc749d162a7cae17f54f94e6bab7c9c46729.

6 release-owned page(s) also affected — read-only, see AGENTS.md Documentation Guardrails.

What this run could not see
  • 1 changed file(s) yielded no anchor (packages/lint/src/index.ts) — pages documenting those are invisible to this run
  • the SDK route bridge reached 60 of 216 client-bound route-ledger rows — the other 156 have no registrar path: tail to select them, so pages documenting THEIR client methods cannot appear above, on this or any run. Of those 156: 0 are remediable by widening that discovery convention (an in-repo file declares the path; the convention did not scan it); 56 are structural — on a ledger where NOT ONE row is declared in-repo, so no discovery change reaches them at any price; 100 are undecided (no in-repo declaration, on a ledger that has other in-repo registrars — absence and an unreadable spelling are not distinguishable here). The rows themselves: node scripts/docs-audit/affected-docs.mjs --bridge-coverage
  • a page that states a rule by its inputs shares no identifier with the emitter that implements the rule, so an emitter-only diff cannot list it — not on this run and not on any run. Measured on fix(driver-sql): emit varchar(maxLength) for a text field a declared index keys on #11430: content/docs/protocol/objectql/types.mdx documents the text-family column mapping by the ObjectQL type names it maps FROM (text / textarea / html) while the diff changed createColumn; it went unlisted, and it was the page that diff falsified, in four places. No shared token exists to detect this on, so a rule your change carries has to be re-read by hand in the pages that restate it.

Coarse fallback — 5 page(s) merely mention a changed package (the pre-#9192 predicate, kept for the deliberately-wide backstop): node scripts/docs-audit/affected-docs.mjs --json c0e4bc749d162a7cae17f54f94e6bab7c9c46729packageMentionDocs.

Which tree this was computed on

This run read content/docs from 7d9d16fd9ab0b80d71120a3272e86b3e871cce72 — the merge of head 530469ff4ce8c690570e5196dc4102ac1ca7c1e6 into base c0e4bc749d162a7cae17f54f94e6bab7c9c46729, which is what actions/checkout gives a pull_request run. Not the PR head.

A worktree cut from an older main holds a different content/docs, so re-deriving there can legitimately return a different list — that is a different tree, not a wrong row. To answer on the same tree:

# while this PR is open — GitHub drops the merge commit once it closes
git fetch origin 7d9d16fd9ab0b80d71120a3272e86b3e871cce72 && git checkout 7d9d16fd9ab0b80d71120a3272e86b3e871cce72
# afterwards, rebuild it from the two parents, which stay fetchable
git fetch origin c0e4bc749d162a7cae17f54f94e6bab7c9c46729 530469ff4ce8c690570e5196dc4102ac1ca7c1e6 && git checkout -B drift-repro c0e4bc749d162a7cae17f54f94e6bab7c9c46729 && git merge --no-ff 530469ff4ce8c690570e5196dc4102ac1ca7c1e6

node scripts/docs-audit/affected-docs.mjs --json c0e4bc749d162a7cae17f54f94e6bab7c9c46729

⚠️ That checkout carried uncommitted changes, so the commit above does not fully identify what was read.

Advisory only, and a precision-first one (#9192): a page is listed because it names a
symbol, wire route or SDK method this diff touched — not because it mentions a changed
package. Each row says which anchor put it there, so a wrong row is reportable rather than
merely annoying. To re-verify, run the docs-accuracy-audit workflow scoped to these files:
node scripts/docs-audit/affected-docs.mjs c0e4bc749d162a7cae17f54f94e6bab7c9c46729 → pass the list as
args.docs, on the commit named under Which tree this was computed on.

…th directions

The rule's detection was right; what it SAID the miss costs was not. The
consequence prose claimed both reference misses fail closed. That holds for an
unresolved current_user value, which the compiler refuses in every position, and
it does not hold for a missing field: extractTargetField recognises only a
leading `field ==` / `=` / `in`, so a negation or any later arm leaves the policy
kept, and a row without that column satisfies the negated constraint. Measured
3/3 rows against a 1/3 real narrowing and a 0/3 phantom positive, on the read
path and on matchesFilterCondition alike.

The message, the docblock and the changeset now say which direction applies, and
say it with the limits intact: not a cross-tenant leak, driver-sql not measured.
The runtime repair is tracked separately and is not attempted here.

Also tightens the current_user position pin, which asserted a disjunction that
passed on whichever position happened to be silent.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_012GKcPZbMoGq7WPzKLfRBTU
check:doc-authoring is right: a runtime string reaches authors, operators and
generated surfaces, none of whom can resolve a bare issue number. The id moves
to the adjacent comment, where the reader who can resolve it is already looking,
and a test pins that neither the message nor the hint carries one again.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_012GKcPZbMoGq7WPzKLfRBTU

os-bill commented Sep 9, 2026

Copy link
Copy Markdown
Collaborator

Director seat adoption record — POST-MERGE audit — summon #20, session_01Tep4AYXZvyBA7jsvne5KZV (os-bill), 2026-09-09T07:10Z. This PR was flipped ready (04:44:58Z), enqueued (04:46:19Z) and merged (05:11:12Z, e4fd55d9b0) by baozhoutao (the domain:devx seat, session_012GKcPZbMoGq7WPzKLfRBTU) with needs:contract-review still on the PR. The landed note on card #16119 (5596178544) asserts an at-tier review FAIL followed by a patch round, but no review text exists on the PR (0 reviews, 0 inline comments) or on the card; a verdict that is not on record is not on record. The PR still carries the carrier. The verdict below is the record at the mandated tier: adopted verbatim from an isolated contract-review subagent (explicit model = CONTRACT_REVIEW_TIER). Transcript tier check before adoption: every harness-stamped model field in the subagent transcript reads claude-fable-5-1 (103 stamps, no other value). Merged head re-read at posting time = 530469ff4c.


Contract review (CONTRACT_REVIEW_TIER, isolated seat, POST-MERGE) — PR #17036 @ 530469ff4ce8c690570e5196dc4102ac1ca7c1e6

Verdict: PASS WITH FINDINGS

The landed code is correct, conforms to the dispatch, and needs no code follow-up. What is owed is card-side: #17042 (the runtime fail-open this PR surfaced) is sitting unlabelled three hours after filing, its scope omits a write face that is established on the source, and the triage escalation clause (p1 + security) has been applied by nobody. Details in F1.

Increment verified: git diff e4fd55d9b0^1 e4fd55d9b0 is byte-identical to git diff 2e8e1185b5 refs/pr-review/17036 (911 lines; squash merge, single parent 86f42461ad). Four files: .changeset/rls-predicate-references.md (+24), packages/lint/src/index.ts (+6), packages/lint/src/validate-rls-predicate-enforceability.ts (+423/−8), its .test.ts (+356).

Ruling conformance

Ruling (card 5556554578 / claim 5595026502) Reading on e4fd55d9b0
Claim before code; dev inherits, posts no second claim Claim 5595026502 names session_012GKcPZbMoGq7WPzKLfRBTU + branch; all 5 non-merge commits carry that Claude-Session; no second claim. ✓
Do not modify the three shape rules RLS_PREDICATE_UNENFORCEABLE :175, _UNPARSEABLE :177, _OVER_BUDGET :179 unchanged; the shape branch's only edit hoists where/path to whereEarly/pathEarly with identical expressions (:672-679, :703-704). Behaviour-preserving. ✓
current_user.* set DERIVED, not transcribed PRERESOLVED_USER_KEYS = new Set(RESERVED_RLS_MEMBERSHIP_KEYS) (:327) from @objectstack/spec/contracts (rls-membership-resolver.ts:89-96). hotcrm's five-name list not copied. ✓
Do not retire/reference hotcrm assertions No hotcrm path in the diff; PR body says untouched. ✓
Escalation clause: report, do not act/widen Reported (5595933739), filed as #17042, not widened into the PR. ✓ on the dev's side — but see F1: the clause's action was never taken.
File surface: rule file + tests + id enumerations + .changeset/ Exactly that; index.ts edit is 2 names + a comment (:358-359). authoring-rules.ts deliberately untouched and flagged. ✓
Card relation in PR body only, no commit trailers Part of #16119 in body; no Refs/Part of in any commit message. ✓
Draft PR, PM lands Enqueued 04:46:19Z, merged 05:11:12Z by baozhoutao via merge queue.
Acceptance 1–6 All four injections pinned (it.each table); before-leg silence accepted from the card's own measurement (two firing controls at the same site) and pinned mechanically post-hoc by "NOTHING else in the whole rule table reports either miss"; negative controls pinned; messages name the cost; corpus zeros with controls (two of three corrected to "not readings"); publish set measured. ✓
Contract review before landing No contract-review text exists on the PR (reviews [], review comments [], 1 bot comment) or on the card. The landed note's "At-tier contract review FAIL → one patch round" is unverifiable; the "reviewer" and the dev share one session id (session_012GKcPZbMoGq7WPzKLfRBTU on the commits, the claim, and the #17042 filing). Merged with needs:contract-review still on the PR (it is on the PR now; the card no longer carries it — a split pair). This review is the record.

Derived judgments

(a) Rule ids, level, predicate — and "no second parser".

  • rls-predicate-unknown-field (:184) and rls-predicate-unknown-user-variable (:189), both severity: 'error' (:592, :613), in the gating tier.
  • Both run only inside if (isSupportedRlsExpression(source)) (:680) → disjoint from the three shape ids by construction (test pins it).
  • Field half: referenceFindings (:574) calls sqlPredicateToCel then compileCelToFilter (formula's own, cel-to-filter.ts:221) against a probe context and reads field paths off the lowered FilterCondition (filterFieldPaths :480). Verified every key producer in the compiler keys on the field path — emit (:421), lowerMembership (:362), lowerStringMethod (:404) — field-to-field emits { $field } (:346), and the only combinators are $and/$or/$not (:296-302), which is exactly what filterFieldPaths walks. record.x classifies as field x (fieldRoots default ['record'], :437-475), so no false positive from the prefix. Verdicts via resolveFieldPath; isUnjudgeable skips unknowable (object-not-in-stack / no-field-map / injected-hop) and describeFieldPathVerdict returns nothing for ok — the three skips are the graph's. No second parse of the predicate; the only parsing in lint is of the compiler's detail string (see F7).
  • Variable half: discovered from the compiler's own unresolved-variable refusal (resolveValue, cel-to-filter.ts:491-508), position decided by re-compiling with a scalar probe (userVariableIsScalarPositioned :386); in-position keys are never reported (§7.3.1). Pre-resolved set = RESERVED_RLS_MEMBERSHIP_KEYS = id, organization_id, positions, org_user_ids, accessible_org_ids, email — a strict superset of the five RLSCompiler.compileFilter binds (rls-compiler.ts:272-279: no accessible_org_ids). Therefore silent on accessible_org_ids in both positions (test loops every reserved key, both positions, asserted separately) — [finding] current_user.accessible_org_ids is reserved and ADR-0105 D11 calls it core-resolved, but nothing resolves it into the RLS variable bag — every predicate using it fails closed #16518 stays untouched. ✓

(b) Doors and dep tier. authoring-rules.ts:1525-1537: tier: 'gating', input: 'parsed', commands: ALL, surfaces: CLI_ONLY. Runs under os validate (validate.ts:302), os compile (compile.ts:383, the 'build' axis — the changeset's "os compile" is accurate), os lint (lint.ts:514). Not run at runtime-publish (CLI_ONLY), though the module is bundled into dist/runtime.* via runtime-gate.ts:57 → authoring-rules.js. @objectstack/spec/contracts is the first such import in lint src, but lint already imports the @objectstack/spec root (6 files) and 8 other subpaths; LAZY_DEPS is typescript/sucrase/ajv/ajv-formats only; lean-entry-closure rides Lint & Repo Gates (green). Dep-tier contract respected. ✓

(c) Consequence prose vs. runtime source. Verified: extractTargetField (security-plugin.ts:7539) is ^\s*([a-z_][a-z0-9_]*)\s*(?:==|=|IN|in)(?=\s|\() — leading only, !=/! deliberately unrecognised; if (!targetField) return true keeps the policy (:5910); dropped++ only for an extracted-and-missing field (:5915); sentinel only when layer1 == null && dropped > 0 (:5921-5923). noValueSatisfiesNegation (memory-matcher.ts:161) = $ne/$nin/$notContains, admitted through the pre-switch guard (:393). Variable half: resolveValue throws in every position; compileFilter collects (:318-345) → filters.length === 0RLS_DENY_FILTER, one WARN via warnFailClosedDenial (:357). All as the shipped message states. The write face is also established on the source: matchesFilterCondition is imported at security-plugin.ts:109 and evaluated against the post-image at :2822 inside step 3.6 (ADR-0058 D4); computeWriteCheckFilter (:6102-6146) compiles check clauses with no extractTargetField net; in matches-filter.ts $ne!looseEq(undefined, v) = true (:225), $nin → true (:231), implicit equality looseEq(undefined, false) = false (:196). So a negated phantom permits, a positive phantom refuses — the PR message's write-path claim is right. Two precision nits: F2. Escalation clause: partially honoured — F1.

(d) Pre-existing ids untouched — yes (see conformance table).

(e) Published surface / keys / codes / doc-authoring. New exports from @objectstack/lint index: RLS_PREDICATE_UNKNOWN_FIELD, RLS_PREDICATE_UNKNOWN_USER_VARIABLE (:358-359); two new members of the closed rule-id set that --json consumers and allowlists key on. No new payload keys, no spec change, no new error codes beyond the two ids. Repo-wide, nothing else enumerates rls-predicate-* ids (only CHANGELOG.md and index.ts; content/docs has zero mentions), so no catalog/doc page was left stale. check:doc-authoring: #17042 lives only in a // comment (:533); message/hint carry no #\d{3,} (test-pinned); scripts/doc-authoring-prose-id.baseline.json untouched (git diff --stat empty; no entry for this file). ✓

Semver / changeset

Boundary flags

Findings

CI at merge

Head 530469ff4c: 41 check runs, 34 distinct names (7 re-runs on enqueue). Latest-per-name: 29 success, 5 skipped, 0 failure — skipped: Auto Label, Build Docs, Check PR Size, Console Pin Gate, Packed-tarball smoke (opt-in) (path-filtered/opt-in). All 7 ruleset-required contexts success: TypeScript Type Check, Test Core, Dogfood Regression Gate, Build Core, Temporal Conformance (live PG + MySQL), Lint & Repo Gates, Governed Surface Queue Guard. Combined status: Vercel success. Dev-report open_questions: [] in both reports. out_of_scope_findings answered: #16518 exists (open, p1+security, domain:services) and the superset read keeps this rule silent on it — confirmed; surfaceReason staleness — confirmed (F6); reference-integrity-suite.test.ts "four rules" comment — doc nit, no gate reads it; escalation correction — confirmed and extended (F1); corpus double-count correction — accepted (PR body table now states judgeability).

Implemented-by: branch claude/issue-16119-rls-predicate-references (merged as e4fd55d9b0)
Reviewed-by: director seat summon #20 (isolated fable subagent, transcript-verified before adoption)

{"pr":17036,"head":"530469ff4ce8c690570e5196dc4102ac1ca7c1e6","verdict":"PASS WITH FINDINGS","blocking":[],"clause2":"yes","semver_ok":true,"governed":false,"ci":"34 names latest-per-name: 29 success, 5 skipped (path-filtered/opt-in), 0 failed; all 7 ruleset-required contexts success"}


Generated by Claude Code

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants